5장. 연산자
변수에 값을 담았다면 이제 그 값으로 계산을 해야 한다. Go 의 연산자는 C 계열 언어를 써 본 사람에게 거의 익숙하지만, 몇 가지 함정과 특이한 점이 있다.
목표:
- 산술 / 비교 / 논리 연산자의 동작 방식 익히기
- 정수 나눗셈과 오버플로우의 함정 이해하기
- 연산자 우선순위를 큰 그림으로 잡기
5.1 산술 연산자
기본 다섯 가지다.
| 연산자 | 의미 | 예 |
|---|---|---|
+ | 덧셈 | 3 + 2 → 5 |
- | 뺄셈 | 3 - 2 → 1 |
* | 곱셈 | 3 * 2 → 6 |
/ | 나눗셈 | 7 / 2 → ? |
% | 나머지 | 7 % 2 → 1 |
a := 10
b := 3
fmt.Println(a + b) // 13
fmt.Println(a - b) // 7
fmt.Println(a * b) // 30
fmt.Println(a / b) // 3
fmt.Println(a % b) // 1
정수 나눗셈의 함정
위 표에서 7 / 2 의 답을 3.5 로 생각했다면 함정에 빠진 거다.
정수끼리의 나눗셈은 결과도 정수다. 소수점 아래는 그냥 잘려 나간다.
x := 7 / 2 // x 는 3, 3.5 아님
y := 7.0 / 2.0 // y 는 3.5
z := float64(7) / 2 // z 는 3.5
실수 결과가 필요하면 피연산자 중 하나라도 실수여야 한다.
음수 나머지 동작
% 연산자는 음수를 만나면 직관과 살짝 다를 수 있다.
Go 에서는 결과의 부호가 좌변(피제수) 을 따른다.
fmt.Println( 7 % 3) // 1
fmt.Println(-7 % 3) // -1
fmt.Println( 7 % -3) // 1
fmt.Println(-7 % -3) // -1
수학적으로 “양수 나머지” 를 원할 때는
((a % b) + b) % b 같은 식이 필요하다.
오버플로우는 wrap-around
정수형은 표현할 수 있는 범위가 정해져 있다. 그 범위를 넘으면 어떻게 될까?
C 처럼 그냥 한 바퀴 돌아 버린다. (wrap-around)
var n int8 = 127
n = n + 1
fmt.Println(n) // -128
int8 의 최대값은 127이고, 거기에 1 을 더하면
정수가 한 바퀴 돌아 최소값 -128 이 된다.
런타임 에러가 나는 게 아니다. 그냥 조용히 잘못된 값이 생긴다.
“수치가 클 수 있다” 싶으면 int64 같은 넉넉한 타입을 쓰자.
부호 / 부호 없음
산술 연산자는 정수와 실수 모두에 쓴다.
하지만 +, -, *, / 만 실수에 쓸 수 있고,
% (나머지) 는 정수에만 쓸 수 있다.
fmt.Println(7.5 % 2.0) // 컴파일 에러
5.2 비교 연산자
두 값을 비교해서 bool (참 / 거짓) 을 돌려준다.
| 연산자 | 의미 |
|---|---|
== | 같다 |
!= | 다르다 |
< | 작다 |
> | 크다 |
<= | 작거나 같다 |
>= | 크거나 같다 |
a, b := 3, 5
fmt.Println(a == b) // false
fmt.Println(a != b) // true
fmt.Println(a < b) // true
fmt.Println(a >= b) // false
문자열도 비교 가능
문자열은 사전식으로 비교된다. 앞에서부터 한 글자씩 코드값을 비교해 가는 방식이다.
fmt.Println("apple" == "apple") // true
fmt.Println("apple" < "banana") // true
fmt.Println("apple" < "Banana") // false ('B' 가 'a' 보다 작음)
대문자가 소문자보다 코드값이 작다는 점에 주의.
서로 다른 타입은 비교 불가
비교 연산자도 같은 타입끼리만 쓸 수 있다.
var a int = 3
var b int64 = 3
fmt.Println(a == b) // 컴파일 에러
비교하려면 한쪽을 변환해야 한다.
fmt.Println(int64(a) == b) // true
이 점이 답답해 보일 수 있지만, “의도하지 않은 비교” 로 생기는 버그를 막아 준다.
5.3 논리 연산자
bool 값들끼리 조합할 때 쓴다.
| 연산자 | 의미 |
|---|---|
&& | 그리고 (AND) |
|| | 또는 (OR) |
! | 부정 (NOT) |
a, b := true, false
fmt.Println(a && b) // false
fmt.Println(a || b) // true
fmt.Println(!a) // false
단축 평가 (short-circuit)
&& 와 || 는 왼쪽부터 평가하다가
결과가 확정되면 오른쪽은 아예 보지 않는다.
// && 의 단축 평가
// 왼쪽이 false 면 오른쪽은 보지 않는다
if x != 0 && 100/x > 5 {
...
}
위 코드에서 x 가 0 이면
x != 0 이 false 라서 오른쪽 100/x 는 실행되지 않는다.
0 으로 나누는 런타임 에러를 막아 준다.
|| 도 비슷하다.
왼쪽이 true 면 오른쪽은 평가하지 않는다.
if name == "" || isInvalid(name) {
...
}
이 패턴은 실전에서 매우 자주 쓰인다.
5.4 대입 연산자
기본은 = 다.
x := 10
x = 20 // x 에 20 을 다시 대입
산술과 결합된 형태도 자주 쓴다.
| 연산자 | 의미 | 풀어 쓰면 |
|---|---|---|
+= | 더해서 대입 | x = x + y |
-= | 빼서 대입 | x = x - y |
*= | 곱해서 대입 | x = x * y |
/= | 나누어서 대입 | x = x / y |
%= | 나머지로 대입 | x = x % y |
x := 10
x += 5 // x 는 15
x *= 2 // x 는 30
x %= 7 // x 는 2
증감 연산자
x++ 와 x-- 도 있다.
하지만 Go 의 증감 연산자는 다른 언어와 결이 다르다.
Go 의
x++,x--는 식(expression) 이 아니라 문(statement) 이다.
- 다른 식 안에 끼워 넣지 못한다
- 후위 형태만 있다 (전위
++x같은 건 없다)
x := 10
x++ // OK. x 는 11
x-- // OK. x 는 10
y := x++ // 컴파일 에러
fmt.Println(x++) // 컴파일 에러
C 같은 언어에서 흔히 보던
a = b++ 같은 영리한 코드는 Go 에선 쓸 수 없다.
이 점이 처음엔 어색하지만,
“증감은 한 줄짜리 동작” 이라는 단순한 규칙을 강제해
가독성을 지킨다.
5.5 비트 연산자
비트 단위로 정수를 다루는 연산자다. 처음에는 자주 쓸 일이 없지만 알아 두면 좋다.
| 연산자 | 의미 |
|---|---|
& | AND (둘 다 1) |
| | OR (하나라도 1) |
^ | XOR (둘이 다를 때 1) |
<< | 왼쪽 시프트 |
>> | 오른쪽 시프트 |
&^ | AND NOT (Go 특유) |
a := 0b1100 // 12
b := 0b1010 // 10
fmt.Println(a & b) // 8 (0b1000)
fmt.Println(a | b) // 14 (0b1110)
fmt.Println(a ^ b) // 6 (0b0110)
fmt.Println(a << 1) // 24
fmt.Println(a >> 1) // 6
&^ (AND NOT) 연산자
다른 언어엔 잘 없는 Go 특유의 연산자다.
a &^ b는 “a 의 비트 중에서 b 가 1 인 자리만 끄기” 다.a & (^b)와 같다.
a := 0b1101 // 13
b := 0b0100 // 4
fmt.Println(a &^ b) // 0b1001 → 9
“플래그 비트를 끄고 싶을 때” 종종 등장한다. 지금은 “이런 게 있다” 정도만 기억하자.
5.6 연산자 우선순위
Go 의 우선순위는 다섯 단계로 단순하게 정리돼 있다. 위쪽일수록 먼저 계산된다.
| 우선순위 | 연산자 |
|---|---|
| 1 (가장 높음) | *, /, %, <<, >>, &, &^ |
| 2 | +, -, |, ^ |
| 3 | ==, !=, <, <=, >, >= |
| 4 | && |
| 5 (가장 낮음) | || |
수학적인 직관과 대체로 맞다. 곱셈/나눗셈이 덧셈/뺄셈보다 먼저고, 산술이 비교보다 먼저고, AND 가 OR 보다 먼저다.
result := 3 + 4 * 2 // 11 (3 + 8), 14 아님
ok := a > 0 && b > 0 // (a > 0) && (b > 0)
헷갈리면 괄호로
우선순위를 외우려 애쓰기보다, 조금만 복잡하면 그냥 괄호를 치는 게 낫다.
// 동작은 같지만 의도가 더 분명함
result := 3 + (4 * 2)
ok := (a > 0) && (b > 0)
코드를 읽는 사람도, 미래의 본인도 고마워한다.
5.7 정리
- 산술 연산자에서 가장 자주 실수하는 곳은 정수 나눗셈(
/)- 정수 / 정수 = 정수
- 실수 결과가 필요하면 피연산자를 실수로
- 비교 연산자는
bool을 돌려준다, 다른 타입끼리는 비교 불가 - 논리 연산자(
&&,||)는 단축 평가를 한다 x++,x--는 문이지 식이 아니다 (후위만 가능)- 비트 연산자에는 Go 특유의
&^(AND NOT) 가 있다 - 우선순위 외우기 어렵다면 괄호로 명시하자
값과 연산이 갖춰졌다. 다음 장에서는 그동안 가볍게만 다뤘던 문자열을 본격적으로 들여다본다. 한글이 등장하면서 흥미로운 함정이 몇 개 나온다.